/*
 * Copyright (C) 2011  rafael1193
 *
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>. 
 *
 * Programador: rafael1193
 * 
 * Fecha: 16/07/2011
 * 
 */

using System;
using System.Text;
using System.Collections.Generic;
using System.Linq;
using System.IO;

namespace SDAT
{
    class STRM
    {
        public static sSTRM LeerArchivo(string path)
        {
            return LeerArchivo(File.ReadAllBytes(path));
        }
        public static sSTRM LeerArchivo(byte[] path)
        {
            sSTRM strm = new sSTRM();
            BinaryReader br = new BinaryReader(new MemoryStream(path));

            // Genérico
            strm.cabecera.id = br.ReadChars(4);
            strm.cabecera.constant = br.ReadUInt32();
            strm.cabecera.fileSize = br.ReadUInt32();
            strm.cabecera.headerSize = br.ReadUInt16();
            strm.cabecera.nBlocks = br.ReadUInt16();
            // HEAD section
            strm.head.id = br.ReadChars(4);
            strm.head.size = br.ReadUInt32();
            strm.head.waveType = br.ReadByte();
            strm.head.loop = br.ReadByte();
            strm.head.channels = br.ReadUInt16();
            strm.head.sampleRate = br.ReadUInt16();
            strm.head.time = br.ReadUInt16();
            strm.head.loopOffset = br.ReadUInt32();
            strm.head.nSamples = br.ReadUInt32();
            strm.head.dataOffset = br.ReadUInt32();
            strm.head.nBlocks = br.ReadUInt32();
            strm.head.blockLen = br.ReadUInt32();
            strm.head.blockSample = br.ReadUInt32();
            strm.head.lastBlocklen = br.ReadUInt32();
            strm.head.lastBlockSample = br.ReadUInt32();
            br.ReadBytes(32); // Reserved
            // DATA section
            strm.data.id = br.ReadChars(4);
            strm.data.size = br.ReadUInt32();
            strm.data.data = br.ReadBytes((int)strm.data.size - 0x08);

            br.Close();

            return strm;
        }

        public static void EscribirArchivo(sSTRM strm, string path)
        {
            /*System.IO.FileStream fs = null;
            System.IO.BinaryWriter bw = null;

            try
            {
                fs = new System.IO.FileStream(path, System.IO.FileMode.Create);
                bw = new System.IO.BinaryWriter(fs);

                //Escribir File
                bw.Write(Encoding.ASCII.GetBytes(strm.file.type));
                bw.Write(strm.file.magic);
                bw.Write(strm.file.nFileSize);
                bw.Write(strm.file.nSize);
                bw.Write(strm.file.nBlock);

                //Escribir Header
                bw.Write(Encoding.ASCII.GetBytes(strm.head.type));
                bw.Write(strm.head.nSize);
                bw.Write(strm.head.nWaveType);
                bw.Write(strm.head.bLoop);
                bw.Write(strm.head.nChannel);
                bw.Write(strm.head.nSampleRate);
                bw.Write(strm.head.nTime);
                bw.Write(strm.head.nLoopOffset);
                bw.Write(strm.head.nSample);
                bw.Write(strm.head.nDataOffset);
                bw.Write(strm.head.nBlock);
                bw.Write(strm.head.nBlockLen);
                bw.Write(strm.head.nBlockSample);
                bw.Write(strm.head.nLastBlockLen);
                bw.Write(strm.head.nLastBlockSample);
                bw.Write(strm.head.reserved);

                //Escribir Data
                bw.Write(Encoding.ASCII.GetBytes(strm.data.type));
                bw.Write(strm.data.nSize);
                bw.Write(strm.data.data);
            }
            catch (Exception ex)
            {
                System.Console.WriteLine(ex.Message.ToString());
            }
            finally
            {
                if (fs != null) fs.Close();
                if (bw != null) bw.Close();
            }*/
        }

        public static WAV.ArchivoWAV ConvertirAWAV(sSTRM strm)
        {
            WAV.ArchivoWAV wav = new WAV.ArchivoWAV();

            if (strm.head.channels == 2)
            {
                strm.data.leftChannel = SepararCanales(strm.data.data, strm.head.nBlocks, strm.head.blockLen,
                    strm.head.lastBlocklen, true, strm.head.waveType);
                strm.data.rightChannel = SepararCanales(strm.data.data, strm.head.nBlocks, strm.head.blockLen,
                    strm.head.lastBlocklen, false, strm.head.waveType);
                Array.Clear(strm.data.data, 0, strm.data.data.Length); // Borramos datos no necesarios

                strm.data.data = AlternarCanales(strm.data.leftChannel, strm.data.rightChannel);
            }
            else if (strm.head.channels == 1)
            {
                strm.data.data = ObtenerCanal(strm.data.data, strm.head.nBlocks, strm.head.blockLen,
                    strm.head.lastBlocklen, strm.head.waveType);
            }

            if (strm.head.waveType == 0)
            {
                wav = WAV.GenerarWAVPCM(strm.head.channels, strm.head.sampleRate, 8, strm.data.data);
            }

            if (strm.head.waveType == 1)
            {
                wav = WAV.GenerarWAVPCM(strm.head.channels, strm.head.sampleRate, 16, strm.data.data);
            }

            if (strm.head.waveType >= 2)
            {
                wav = WAV.GenerarWAVADPCM(strm.head.channels, strm.head.sampleRate, 16, strm.data.data);
            }
            return wav;
        }
        /// <summary>
        /// Write a STRM structure to a file
        /// </summary>
        /// <param name="strm">STRM structure to write</param>
        /// <param name="path">File to write</param>
        public static void Write(sSTRM strm, string path)
        {
            System.IO.FileStream fs = null;
            System.IO.BinaryWriter bw = null;

            try
            {
                fs = new System.IO.FileStream(path, System.IO.FileMode.Create);
                bw = new System.IO.BinaryWriter(fs);

                // Common header
                bw.Write(Encoding.ASCII.GetBytes(strm.cabecera.id));
                bw.Write(strm.cabecera.constant);
                bw.Write(strm.cabecera.fileSize);
                bw.Write(strm.cabecera.headerSize);
                bw.Write(strm.cabecera.nBlocks);

                // HEAD section
                bw.Write(Encoding.ASCII.GetBytes(strm.head.id));
                bw.Write(strm.head.size);
                bw.Write(strm.head.waveType);
                bw.Write(strm.head.loop);
                bw.Write(strm.head.channels);
                bw.Write(strm.head.sampleRate);
                bw.Write(strm.head.time);
                bw.Write(strm.head.loopOffset);
                bw.Write(strm.head.nSamples);
                bw.Write(strm.head.dataOffset);
                bw.Write(strm.head.nBlocks);
                bw.Write(strm.head.blockLen);
                bw.Write(strm.head.blockSample);
                bw.Write(strm.head.lastBlocklen);
                bw.Write(strm.head.lastBlockSample);
                bw.Write(strm.head.reserved);

                // DATA section
                bw.Write(Encoding.ASCII.GetBytes(strm.data.id));
                bw.Write(strm.data.size);
                bw.Write(strm.data.data);

                bw.Flush();
            }
            catch (Exception ex)
            {
                System.Console.WriteLine(ex.Message.ToString());
            }
            finally
            {
                if (bw != null) bw.Close();
                if (fs != null) fs.Close();
            }
        }
        /// <summary>
        /// Convert a WAV structure to a STRM structure
        /// </summary>
        /// <param name="wav">WAV structure to convert</param>
        /// <returns>STRM structure converted</returns>
        public static sSTRM ConvertToSTRM(WAV.ArchivoWAV wav)
        {
            if (wav.wave.fmt.audioFormat != WAV.ArchivoWAV.WaveChunk.FmtChunk.WaveFormat.WAVE_FORMAT_PCM)
                throw new NotSupportedException();

            sSTRM strm = new sSTRM();
            strm.cabecera.id = "STRM".ToArray();
            strm.cabecera.constant = 0x0100FEFF;
            strm.cabecera.headerSize = 0x10;
            strm.cabecera.nBlocks = 0x02;

            strm.head.id = "HEAD".ToArray();
            strm.head.size = 0x50;
            strm.head.waveType = 1;
            strm.head.loop = 0;
            strm.head.channels = wav.wave.fmt.numChannels;
            strm.head.sampleRate = (ushort)wav.wave.fmt.sampleRate;
            strm.head.time = (ushort)(1.0 / strm.head.sampleRate * 1.6756991e+7 / 32);
            strm.head.loopOffset = 0x00;
            strm.head.dataOffset = 0x68;
            strm.head.reserved = new Byte[32];

            strm.data.id = "DATA".ToArray();
            if (wav.wave.fmt.numChannels == 2)
            {
                strm.data.leftChannel = DivideChannels(wav.wave.data.data, true, 0);
                strm.data.rightChannel = DivideChannels(wav.wave.data.data, false, 0);
                byte[][] leftBlock = CreateBlocks(strm.data.leftChannel);
                byte[][] rigthBlock = CreateBlocks(strm.data.rightChannel);
                strm.data.data = MergeBlocks(leftBlock, rigthBlock);

                strm.head.blockLen = (uint)leftBlock[0].Length;
                strm.head.blockSample = strm.head.blockLen / 2;
                strm.head.lastBlocklen = (uint)leftBlock[leftBlock.Length - 1].Length;
                strm.head.lastBlockSample = strm.head.lastBlocklen / 2;
                strm.head.nBlocks = (uint)leftBlock.Length;
                strm.head.nSamples = strm.head.nBlocks * 0x100 + strm.head.lastBlockSample;
            }
            else
            {
                byte[][] blocks = CreateBlocks(wav.wave.data.data);
                strm.head.blockLen = (uint)blocks[0].Length;
                strm.head.blockSample = strm.head.blockLen / 2;
                strm.head.lastBlocklen = (uint)blocks[blocks.Length - 1].Length;
                strm.head.lastBlockSample = strm.head.lastBlocklen / 2;
                strm.head.nBlocks = (uint)blocks.Length;
                strm.head.nSamples = strm.head.nBlocks * 0x100 + strm.head.lastBlockSample;
                strm.data.data = wav.wave.data.data;
            }

            strm.data.size = (uint)strm.data.data.Length + 0x08;
            strm.cabecera.fileSize = strm.data.size + strm.head.size + strm.cabecera.headerSize;

            return strm;
        }
        #region WAV to STRM methods
        /// <summary>
        /// Divide the channels of PCM-16 audio data
        /// </summary>
        /// <param name="data">PCM-16 audio data</param>
        /// <param name="leftChannel">If true, returns the audio data of the left channel, else the right channel</param>
        /// <param name="waveType">The wave type of the audio data (compression)</param>
        /// <returns>A channel audio data</returns>
        static byte[] DivideChannels(byte[] data, bool leftChannel, int waveType)
        {
            List<Byte> channel = new List<byte>();

            // TODO: Add compressions (AD-PCM ,PCM-8)
            for (int i = (leftChannel ? 0 : 2); i < data.Length; i += 4)
            {
                channel.Add(data[i]);
                channel.Add(data[i + 1]);
            }

            return channel.ToArray();
        }
        /// <summary>
        /// Create a block data from audio data. Default length 512 bytes, 256 samples.
        /// </summary>
        /// <param name="channel">Channel audio data</param>
        /// <returns>Block data</returns>
        static byte[][] CreateBlocks(byte[] channel)
        {
            List<Byte[]> blocks = new List<Byte[]>();

            int nBlocks = channel.Length / 0x200;
            int lastBlockLength = channel.Length % 0x200;

            Byte[] block = new Byte[0x200];
            for (int i = 0; i < nBlocks; i++)
            {
                block = new Byte[0x200];
                Array.Copy(channel, i * 0x200, block, 0, 0x200);
                blocks.Add(block);
            }
            block = new Byte[lastBlockLength];
            Array.Copy(channel, nBlocks * 0x200, block, 0, lastBlockLength);
            blocks.Add(block);

            return blocks.ToArray();
        }
        /// <summary>
        /// Merge blocks of two channels.
        /// </summary>
        /// <param name="leftChannel">Left channel block</param>
        /// <param name="rightChannel">Right channel block</param>
        /// <returns>Return the audio data</returns>
        static byte[] MergeBlocks(byte[][] leftChannel, byte[][] rightChannel)
        {
            List<byte> data = new List<byte>();

            for (int i = 0; i < leftChannel.Length; i++)
            {
                data.AddRange(leftChannel[i]);
                data.AddRange(rightChannel[i]);
            }

            return data.ToArray();
        }
        #endregion

        static byte[] SepararCanales(byte[] data, uint nBlocks, uint blockLen, uint lastBlockLen, bool leftChannel, int waveType)
        {
            List<byte> resultado = new List<byte>();
            List<byte> datos = new List<byte>();
            datos.AddRange(data);
            int j = (leftChannel) ? 0 : (int)blockLen;

            byte[] blockData;
            for (int i = 0; i < nBlocks - 1; i++)
            {
                blockData = new byte[(int)blockLen];
                datos.CopyTo(i * (int)blockLen * 2 + j, blockData, 0, (int)blockLen);

                if (waveType == 2)
                    resultado.AddRange(AdpcmDecompressor.DecompressBlock_ADPCM(blockData, BitConverter.ToInt16(blockData, 0), BitConverter.ToInt16(blockData, 2)));
                else if (waveType == 1)
                    resultado.AddRange(blockData);
                else if (waveType == 0)
                    resultado.AddRange(PCM8(blockData));
            }
            blockData = new byte[(int)lastBlockLen];
            j = (leftChannel) ? 0 : (int)lastBlockLen;
            datos.CopyTo((int)(nBlocks - 1) * (int)blockLen * 2 + j, blockData, 0, (int)lastBlockLen);

            if (waveType == 2)
                resultado.AddRange(AdpcmDecompressor.DecompressBlock_ADPCM(blockData, BitConverter.ToInt16(blockData, 0), BitConverter.ToInt16(blockData, 2)));
            else if (waveType == 1)
                resultado.AddRange(blockData);
            else if (waveType == 0)
                resultado.AddRange(PCM8(blockData));

            return resultado.ToArray();
        }
        static byte[] AlternarCanales(byte[] leftChannel, byte[] rightChannel)
        {
            List<byte> resultado = new List<byte>();

            for (int i = 0; i < leftChannel.Length; i += 2)
            {
                resultado.Add(leftChannel[i]);
                if (i + 1 < leftChannel.Length)
                    resultado.Add(leftChannel[i + 1]);

                resultado.Add(rightChannel[i]);
                if (i + 1 < leftChannel.Length)
                    resultado.Add(rightChannel[i + 1]);
            }

            return resultado.ToArray();
        }
        static byte[] ObtenerCanal(byte[] data, uint nBlocks, uint blockLen, uint lastBlockLen, int waveType)
        {
            List<byte> resultado = new List<byte>();
            List<byte> datos = new List<byte>();
            datos.AddRange(data);

            byte[] blockData;
            for (int i = 0; i < nBlocks - 1; i++)
            {
                blockData = new byte[(int)blockLen];
                datos.CopyTo(i * (int)blockLen, blockData, 0, (int)blockLen);

                if (waveType == 2)
                    resultado.AddRange(AdpcmDecompressor.DecompressBlock_ADPCM(blockData, BitConverter.ToInt16(blockData, 0), BitConverter.ToInt16(blockData, 2)));
                else if (waveType == 1)
                    resultado.AddRange(blockData);
                else if (waveType == 0)
                    resultado.AddRange(PCM8(blockData));
            }
            blockData = new byte[(int)lastBlockLen];
            datos.CopyTo((int)(nBlocks - 1) * (int)blockLen, blockData, 0, (int)lastBlockLen);

            if (waveType == 2)
                resultado.AddRange(AdpcmDecompressor.DecompressBlock_ADPCM(blockData, BitConverter.ToInt16(blockData, 0), BitConverter.ToInt16(blockData, 2)));
            else if (waveType == 1)
                resultado.AddRange(blockData);
            else if (waveType == 0)
                resultado.AddRange(PCM8(blockData));

            return resultado.ToArray();
        }
        static byte[] PCM8(byte[] data)
        {
            byte[] resul = new byte[data.Length];

            for (int i = 0; i < data.Length; i++)
            {
                resul[i] = unchecked((byte)(data[i] ^ 0x80));
            }

            return resul;
        }


        public struct sSTRM
        {
            public Cabecera cabecera;
            public HEAD head;
            public DATA data;

            public struct Cabecera
            {
                public char[] id;
                public uint constant;
                public uint fileSize;
                public ushort headerSize;
                public ushort nBlocks;
            }
            public struct HEAD
            {
                public char[] id;
                public uint size;
                public byte waveType;
                public byte loop;
                public ushort channels;
                public ushort sampleRate;
                public ushort time;
                public uint loopOffset;
                public uint nSamples;
                public uint dataOffset;
                public uint nBlocks;
                public uint blockLen;
                public uint blockSample;
                public uint lastBlocklen;
                public uint lastBlockSample;
                public byte[] reserved; //always 0
            }
            public struct DATA
            {
                public char[] id;
                public uint size;
                public byte[] leftChannel;
                public byte[] rightChannel;
                public byte[] data;
            }
        }
    }
}
